package demo.designPattern.proxy;

import demo.spring.service.IUserDAO;
import demo.spring.service.impl.UserDAO;
import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalTime;

/**
 * 动态代理是一种设计模式，它允许在运行时创建代理类，而不是在编译时创建。
 * 这种方式比静态代理更加灵活，因为动态代理可以在执行期间根据需要生成适当的代理类和对象。
 * 在 Java 中，动态代理通常通过 Java 提供的 java.lang.reflect.Proxy 类以及接口来实现。
 * <p>
 * 动态代理的实现步骤
 * <li>定义接口：创建一个接口，制定代理和目标对象需要实现的方法。
 * <li>实现目标类：创建一个实现该接口的类，定义实际的业务逻辑。
 * <li>创建动态代理：使用 Java 的反射机制，通过 Proxy 类和 InvocationHandler 接口来动态生成代理类，并在代理类中添加横切关注点（例如日志、权限验证等）。
 * <p>
 * <p>
 * 动态代理的优缺点
 * 优点：
 * <p>
 * <li>灵活性高：可以在运行时决定代理的行为，不需要为每个目标类创建代理类。
 * <li>简化代码：减少了编写多个代理类的需要，只需实现一个 InvocationHandler 即可。
 * <li>支持多种目标类：不需要在代理类中提前定义目标类的类型，可以通过接口进行处理，适用于多个目标类。
 * 缺点：
 * <p>
 * <li>时间开销：动态代理的创建和调用过程中由于需要反射，所以在性能上会相对静态代理有所下降。
 * <li>只能代理接口：Java 动态代理只能基于接口进行代理，不能代理类。如果需要对类进行代理，可以使用其他库（如 CGLIB）。
 * <p>
 * 使用 CGLIB 实现类的动态代理
 * 如果使用 CGLIB，则可以在运行时动态代理类，而不仅限于接口。它通过生成子类来实现代理。CGLIB 的使用方法略有不同，通常是创建一个 MethodInterceptor。
 * <p>
 * 总结
 * 动态代理是一种灵活且强大的方法，可以用于横切关注点的处理，而不需要修改目标对象的代码。它在 AOP（面向切面编程）、RPC（远程过程调用）等领域有广泛的应用。
 */
public class DynamicProxyDemo {


    /**
     * 动态代理：即在运行期动态创建代理类，使用动态代理实现AOP需要4个角色：
     *
     * <li>被代理的类：即AOP里所说的目标对象
     * <li>被代理类的接口
     * <li>织入器：使用接口反射机制生成一个代理类，在这个代理类中织入代码
     * <li>InvocationHandler切面：切面，包含了Advice和Pointcut
     * <p>
     * 动态代理在运行期通过接口动态生成代理类，这为其带来了一定的灵活性，但这个灵活性却带来了两个问题：
     * <li>第一，代理类必须实现一个接口，如果没实现接口会抛出一个异常
     * <li>第二，性能影响，因为动态代理是使用反射机制实现的，首先反射肯定比直接调用要慢，其次使用反射大量生成类文件可能引起full gc。
     * 因为字节码文件加载后会存放在JVM运行时方法区（或者叫永久代、元空间）中，当方法区满时会引起full gc， 所以当你大量使用动态代理时，可以将永久代设置大一些，减少full gc的次数。
     */
    @Test
    public void testDynamicProxy() {
        // 需要代理的类接口，被代理类实现的多个接口都必须在这这里定义
        Class[] proxyInterface = new Class[]{IUserDAO.class};
        IUserDAO userDAO = new UserDAO();
        // 构建AOP的Advice，这里需要传入业务类的实例
        InvocationHandler invocationHandler = new LogInvocationHandler(userDAO);
        // 生成代理类的字节码加载器
        ClassLoader classLoader = IUserDAO.class.getClassLoader();
        // 织入器，织入代码并生成代理类 。动态创建代理对象，用于代理一个AbstractUserDAO类型的真实主题对象
        IUserDAO proxyInstance = (IUserDAO) Proxy.newProxyInstance(classLoader, proxyInterface, invocationHandler);
        proxyInstance.findUserById("张无忌"); // 调用代理对象的业务方法
    }

    /**
     * 动态代理的核心其实就是代理对象的生成，即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。 核心代码就三行：
     * <li>获取代理类
     * <li>获取带有InvocationHandler参数的构造方法
     * <li>把handler传入构造方法生成实例
     */
    @Test
    public void testDynamicProxy2() {
        IUserDAO userDAO = new UserDAO();
        // 构建AOP的Advice，这里需要传入业务类的实例
        InvocationHandler invocationHandler = new LogInvocationHandler(userDAO);
        // 获取代理类
        Class<?> proxyClass = Proxy.getProxyClass(IUserDAO.class.getClassLoader(), IUserDAO.class);
        // 获取带有InvocationHandler参数的构造方法
        Constructor<?> constructor = null;
        try {
            constructor = proxyClass.getConstructor(InvocationHandler.class);
        } catch (NoSuchMethodException | SecurityException e1) {
            e1.printStackTrace();
        }
        // 把handler传入构造方法生成实例
        IUserDAO userDao = null;
        try {
            userDao = (IUserDAO) constructor.newInstance(invocationHandler);
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                 | InvocationTargetException e) {
            e.printStackTrace();
        }
        userDao.findUserById("张三丰"); // 调用代理对象的业务方法
    }

}

/**
 * InvocationHandler 是一个函数式接口，用于处理动态代理实例的方法调用。
 * <p>
 * 自定义请求处理程序类. 动态代理类只能代理接口，代理类都需要实现InvocationHandler类，实现invoke方法。
 * 该invoke方法就是调用被代理接口的所有方法时需要调用的，该invoke方法返回的值是被代理接口的一个实现类
 */
class LogInvocationHandler implements InvocationHandler {

    private Object target;// 目标对象

    // 自定义有参构造函数，用于注入一个需要提供代理的真实主题对象
    public LogInvocationHandler(Object object) {
        this.target = object;
    }

    // 实现invoke()方法，调用在真实主题类中定义的方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        beforeInvoke();
        Object result = method.invoke(target, args); // 转发调用
        afterInvoke();
        return result;
    }

    // 记录方法调用时间
    public void beforeInvoke() {
        System.out.println("调用开始时间：" + LocalTime.now());
    }

    public void afterInvoke() {
        System.out.println("调用结束时间：" + LocalTime.now());
    }
}
